﻿using System;
using System.Windows;
using System.Windows.Browser;
using System.Windows.Media;

namespace HDViewSL
{
	/// <summary>
	/// An intermediary object that serves as the communication channel between Javascript and
	/// HD View SL.
	/// </summary>
	/// <remarks>See http://research.microsoft.com/en-us/um/redmond/groups/ivm/hdview/HDXMLspec.htm
	/// for a description of the Javascript interface to HD View, some of which is supported by
	/// HD View SL.</remarks>
	public class HDViewSettings
	{
		private Page page;
		private bool isPageReady;
		private bool isDeferringViewUpdate;
		private bool isUpdatingView;
		private float yaw;
		private float pitch;
		private float fov;
		private float xCtr;
		private float yCtr;
		private float zoom;

		/// <summary>
		/// Constructs a new HDViewSettings object to act as communication channel between
		/// Javascript and the specified Page object.
		/// </summary>
		/// <param name="page">A page.</param>
		public HDViewSettings(Page page)
		{
			this.page = page;
			this.page.Ready += this.Page_Ready;
			this.page.ReceivedInput += this.Page_ReceivedInput;
			this.page.ViewChanged += this.Page_ViewChanged;
		}

		/// <summary>
		/// A source URI (or HD View XML).
		/// </summary>
		[ScriptableMember]
		public string FileName
		{
			get { return this.page.Source; }
			set
			{
				this.isPageReady = false;
				this.page.ImageWidth = this.page.ImageHeight = 0;
				this.page.DisplayMode = DisplayMode.Normal;
				this.page.Source = value;
			}
		}

		/// <summary>
		/// The background color, encoded as a 24-bit unsigned integer (0xRRGGBB).
		/// </summary>
		[ScriptableMember]
		[CLSCompliant(false)]
		public uint BackgroundColor
		{
			get { return ColorToUint(this.page.BackgroundColor, false); }
			set { this.page.BackgroundColor = UintToColor(value, false); }
		}

		/// <summary>
		/// The foreground color, encoded as a 32-bit unsigned integer (0xAARRGGBB).
		/// </summary>
		[ScriptableMember]
		[CLSCompliant(false)]
		public uint ForegroundColor
		{
			get { return ColorToUint(this.page.ForegroundColor, true); }
			set { this.page.ForegroundColor = UintToColor(value, true); }
		}

		/// <summary>
		/// The yaw angle in degrees (used only for rotational views).
		/// </summary>
		[ScriptableMember]
		public float Yaw
		{
			get { return this.yaw; }
			set
			{
				this.yaw = value;
				this.UpdateView();
			}
		}

		/// <summary>
		/// The pitch angle in degrees (used only for rotational views).
		/// </summary>
		[ScriptableMember]
		public float Pitch
		{
			get { return this.pitch; }
			set
			{
				this.pitch = value;
				this.UpdateView();
			}
		}

		/// <summary>
		/// The horizontal field of view in degrees (used only for rotational views).
		/// </summary>
		[ScriptableMember]
		public float FOV
		{
			get { return this.fov; }
			set
			{
				this.fov = value;
				this.UpdateView();
			}
		}

		/// <summary>
		/// The horizontal center of view as a fraction of image width (used only in flat views).
		/// </summary>
		[ScriptableMember]
		public float XCtr
		{
			get { return this.xCtr; }
			set
			{
				this.xCtr = value;
				this.UpdateView();
			}
		}

		/// <summary>
		/// The vertical center of view as a fraction of image height (used only in flat views).
		/// </summary>
		[ScriptableMember]
		public float YCtr
		{
			get { return this.yCtr; }
			set
			{
				this.yCtr = value;
				this.UpdateView();
			}
		}

		/// <summary>
		/// The zoom factor (used only in flat views).
		/// </summary>
		[ScriptableMember]
		public float Zoom
		{
			get { return this.zoom; }
			set
			{
				this.zoom = value;
				this.UpdateView();
			}
		}

		/// <summary>
		/// Currently unused.
		/// </summary>
		[ScriptableMember]
		public int PanMode
		{
			get { return 0; }
			set { }
		}

		/// <summary>
		/// Currently unused.
		/// </summary>
		[ScriptableMember]
		public int ToneMode
		{
			get { return 0; }
			set { }
		}

		/// <summary>
		/// Currently unused.
		/// </summary>
		[ScriptableMember]
		public int ProjMode
		{
			get { return 0; }
			set { }
		}

		/// <summary>
		/// A Javascript callback, invoked with parameters (FOV, Yaw, Pitch) whenever
		/// a change occurs in a rotational view.
		/// </summary>
		[ScriptableMember]
		public ScriptObject onRotate { get; set; }

		/// <summary>
		/// A Javascript callback, invoked with parameters(Zoom, XCtr, YCtr) whenever
		/// a change occurs in a flat view.
		/// </summary>
		[ScriptableMember]
		public ScriptObject onTranslate { get; set; }

		/// <summary>
		/// A Javascript callback, invoked with no parameters whenever the page receives
		/// mouse or keyboard input.
		/// </summary>
		[ScriptableMember]
		public ScriptObject onInput { get; set; }

		/// <summary>
		/// Handles the page's Ready event by applying any deferred view changes.
		/// </summary>
		private void Page_Ready(object sender, EventArgs e)
		{
			this.isPageReady = true;
			if (this.isDeferringViewUpdate)
			{
				this.UpdateView();
				this.isDeferringViewUpdate = false;
			}
		}

		/// <summary>
		/// Handles the page's ReceivedInput event by invoking the onInput callback.
		/// </summary>
		private void Page_ReceivedInput(object sender, EventArgs e)
		{
			if (this.onInput != null)
			{
				try
				{
					this.onInput.InvokeSelf();
				}
				catch
				{
				}
			}
		}

		/// <summary>
		/// Updates the current values of (XCtr, YCtr, Zoom) or (Yaw, Pitch, FOV) from the page's center and zoom.
		/// </summary>
		private void Page_ViewChanged(object sender, EventArgs e)
		{
			var logicalBounds = this.page.LogicalBounds;
			var x = (this.page.Center.X - logicalBounds.X) / logicalBounds.Width;
			var y = (this.page.Center.Y - logicalBounds.Y) / logicalBounds.Height;

			var imageSet = this.page.ImageSet;
			if (imageSet == null || (imageSet.Projection == HDViewProjection.Perspective &&
				imageSet.ThetaMin == 0 && imageSet.ThetaMax == 0 && imageSet.PhiMin == 0 && imageSet.PhiMax == 0))
			{
				var imageAspectRatio = logicalBounds.Width / logicalBounds.Height;
				if (imageAspectRatio < 1)
				{
					var elementAspectRatio = this.page.ActualWidth / this.page.ActualHeight;
					this.zoom = (float)(this.page.Zoom / elementAspectRatio);
					this.xCtr = (float)(0.5 + (x - 0.5) * imageAspectRatio);
					this.yCtr = (float)(1 - y);
				}
				else
				{
					this.zoom = (float)this.page.Zoom;
					this.xCtr = (float)x;
					this.yCtr = (float)(0.5 + (0.5 - y) / imageAspectRatio);
				}
				this.fov = this.pitch = this.yaw = 0;
				InvokeHandler(this.onTranslate, this.zoom, this.xCtr, this.yCtr);
			}
			else
			{
				var thetaRange = imageSet.ThetaMax - imageSet.ThetaMin;
				var phiRange = imageSet.PhiMax - imageSet.PhiMin;
				this.fov = (float)(thetaRange / this.page.Zoom);
				this.yaw = (float)(imageSet.ThetaMin + x * thetaRange);
				this.pitch = (float)(imageSet.PhiMin + y * phiRange);
				this.zoom = this.xCtr = this.yCtr = 0;
				InvokeHandler(this.onRotate, this.fov, this.yaw, this.pitch);
			}
		}

		/// <summary>
		/// Updates the page's center and zoom from the current values of (XCtr, YCtr, Zoom) or (Yaw, Pitch, FOV).
		/// </summary>
		private void UpdateView()
		{
			if (!this.isUpdatingView)
			{
				if (!this.isPageReady)
				{
					this.isDeferringViewUpdate = true;
					return;
				}

				this.isUpdatingView = true;
				var logicalBounds = this.page.LogicalBounds;
				var imageSet = this.page.ImageSet;
				if (imageSet == null || (imageSet.Projection == HDViewProjection.Perspective &&
					imageSet.ThetaMin == 0 && imageSet.ThetaMax == 0 && imageSet.PhiMin == 0 && imageSet.PhiMax == 0))
				{
					var imageAspectRatio = logicalBounds.Width / logicalBounds.Height;
					if (imageAspectRatio < 1)
					{
						var elementAspectRatio = this.page.ActualWidth / this.page.ActualHeight;
						this.page.Zoom = this.zoom * elementAspectRatio;
						var x = 0.5 + (this.xCtr - 0.5) / imageAspectRatio;
						var y = 1 - this.yCtr;
						this.page.Center = new Point(logicalBounds.X + x * logicalBounds.Width, logicalBounds.Y + y * logicalBounds.Height);
					}
					else
					{
						this.page.Zoom = this.zoom;
						var x = this.xCtr;
						var y = 0.5 - (this.yCtr - 0.5) * imageAspectRatio;
						this.page.Center = new Point(logicalBounds.X + x * logicalBounds.Width, logicalBounds.Y + y * logicalBounds.Height);
					}
				}
				else
				{
					var thetaRange = imageSet.ThetaMax - imageSet.ThetaMin;
					var phiRange = imageSet.PhiMax - imageSet.PhiMin;
					if (this.fov > 0)
					{
						this.page.Zoom = thetaRange / this.fov;
					}
					var x = (this.yaw - imageSet.ThetaMin) / thetaRange;
					var y = (this.pitch - imageSet.PhiMin) / phiRange;
					this.page.Center = new Point(logicalBounds.X + x * logicalBounds.Width, logicalBounds.Y + y * logicalBounds.Height);
				}
				this.isUpdatingView = false;
			}
		}

		/// <summary>
		/// Calls a Javascript function, passing the specified parameters.
		/// </summary>
		/// <param name="handler">The Javascript function to call.</param>
		/// <param name="a">The first parameter.</param>
		/// <param name="b">The second parameter.</param>
		/// <param name="c">The third parameter.</param>
		private static void InvokeHandler(ScriptObject handler, float a, float b, float c)
		{
			if (handler != null)
			{
				try
				{
					handler.InvokeSelf(a, b, c);
				}
				catch
				{
				}
			}
		}

		/// <summary>
		/// Converts a color into an unsigned integer.
		/// </summary>
		/// <param name="value">The color to convert.</param>
		/// <param name="useOpacity">True if the color's alpha component should be included,
		/// or false if the color's alpha component should be replaced with zero.</param>
		/// <returns>An unsigned integer encoding the red, green, blue, and (optionally) alpha
		/// components of the color.</returns>
		private static uint ColorToUint(Color value, bool useOpacity)
		{
			return ((uint)(useOpacity ? value.A : 0) << 24) | (uint)(value.R << 16) | (uint)(value.G << 8) | (uint)value.B;
		}

		/// <summary>
		/// Converts an unsigned integer into a color.
		/// </summary>
		/// <param name="value">The unsigned integer encoding of a color.</param>
		/// <param name="useOpacity">True if the alpha component should be extracted, from the
		/// unsigned integer, or false if the alpha component is assumed to be 255.</param>
		/// <returns>The color corresponding to the specified unsigned integer.</returns>
		private static Color UintToColor(uint value, bool useOpacity)
		{
			return Color.FromArgb((byte)(useOpacity ? ((value >> 24) & 255) : 255), (byte)((value >> 16) & 255), (byte)((value >> 8) & 255), (byte)(value & 255));
		}
	}
}
